Skip to content

Fix/panel off line#135

Merged
cayossarian merged 15 commits intomainfrom
fix/panel_off_line
Apr 16, 2026
Merged

Fix/panel off line#135
cayossarian merged 15 commits intomainfrom
fix/panel_off_line

Conversation

@cayossarian
Copy link
Copy Markdown
Member


Summary

Adds a broker-connection callback API to SpanMqttClient and makes get_snapshot() raise when the client is not fully live. Integrations (notably Home
Assistant) can now detect panel offline/online transitions with sub-second latency instead of depending on a fallback poll interval. Part of the work
that unblocks the "panel status shows Connected when panel is offline" bug on the integration side.

What's new

  • SpanMqttClient.register_connection_callback(cb) — subscribe to broker connection state edges. cb(False) on disconnect, cb(True) on reconnect, no
    synthetic call at registration time. Returns an idempotent unregister function. Added to SpanPanelClientProtocol.
  • SpanPanelStaleDataError — new exception, derives from SpanPanelError. Exported from the package top level alongside the rest of the exception
    hierarchy.

Breaking change

  • get_snapshot() contract — now raises SpanPanelStaleDataError when the bridge is disconnected or the Homie device is not ready. Previously it
    silently returned a snapshot built from whatever the accumulator happened to hold, which made offline panels indistinguishable from online ones.
    Consumers with a broad except SpanPanelError or except Exception branch already handle this correctly.

Bug fix

  • Stale snapshot dispatch after disconnect — a pending debounce timer scheduled just before a bridge disconnect could fire afterwards, building a
    snapshot from the still-ready() accumulator (panel died without publishing $state=disconnected) and dispatching it to push-streaming subscribers. This
    caused integrations to flip panels back to "online" while the bridge was still down. Fixed by (1) cancelling the pending timer in
    _on_connection_change(False) and (2) gating _dispatch_snapshot with the same liveness predicate as get_snapshot(). Discovered via live testing after
    the callback + exception work landed.

Test plan

  • Full suite: 339 tests pass, 96.2% coverage
  • TestRegisterConnectionCallback (4 tests) — register / append / remove / idempotent unregister
  • TestConnectionEventDispatch (10 tests) — edge-only fan-out, duplicate suppression, exception isolation, mid-iteration unregister, reconnect
    resubscribe + callback, resubscribe-on-duplicate-True regression lock
  • TestGetSnapshotLiveness (6 tests) — all three guards (bridge None, broker disconnected, homie not ready) + live success + inheritance check
  • TestCloseBehavior — close() resets _live edge tracker for reconnect-in-place safety
  • TestStaleSnapshotDispatchGuard (4 tests) — dispatch bails when bridge disconnected / homie not ready, delivers when live, disconnect cancels the
    timer
  • test_protocol_conformance.py — SpanMqttClient still satisfies the widened SpanPanelClientProtocol
  • mypy + pyright clean on changed files
  • Live reproduction + recovery verified against the span Home Assistant integration with a real simulator drop/restore cycle

Version

2.6.0 — minor bump (additive API + breaking contract on get_snapshot()). CHANGELOG.md has the full entry.

Distinct from SpanPanelConnectionError — used by get_snapshot() to
signal that the client is running but data is not currently live.
Widens SpanPanelClientProtocol and adds SpanMqttClient.register_connection_callback
so consumers can subscribe to broker connection state transitions.
Fan-out behavior is added in the next commit.
…back

- Drop redundant bool annotation on _live to match surrounding style
- Document that registration is edge-only, no synthetic initial call
_on_connection_change now emits edge-only notifications to callbacks
registered via register_connection_callback. Duplicate states are
suppressed; exceptions in one subscriber do not break others.
_FakeBridge now inherits from AsyncMqttBridge and _FakeHomie from
HomieDeviceConsumer, bypassing __init__ to avoid I/O setup. This
makes assignments to client._bridge and client._homie pass strict
type checking without type: ignore or cast().
- Comment in _on_connection_change documents that re-subscribe runs on
  every connected=True event (including duplicates), while callback
  fan-out is edge-only.
- Add regression test locking in the resubscribe-on-duplicate-True
  behavior.
- Tighten the reconnect test's topic assertion to an exact match
  against WILDCARD_TOPIC_FMT.
… live

Replaces the silent stale-cache return with explicit failure. Three
distinct messages (client not connected / broker disconnected /
homie not ready) so logs self-diagnose. Breaking change: consumers
that called get_snapshot() while offline now receive an exception
they can catch to drive offline-state logic.
Document that field values are arbitrary and only object identity
is asserted, so future additions to the SpanPanelSnapshot dataclass
can be handled with zero/empty defaults without touching assertions.
Addresses final-review items:
- SpanPanelStaleDataError is now importable from span_panel_api directly,
  matching the pattern of every other exception in the hierarchy.
- close() resets the _live edge tracker so a subsequent connect() on
  the same instance correctly fires a True edge for the new session.
…or in README

- Update SpanPanelClientProtocol row in the protocols table to include
  register_connection_callback.
- Add a Connection State Monitoring section after Streaming Pattern
  showing how to subscribe to broker connection edges, with a note that
  get_snapshot() now raises SpanPanelStaleDataError when not live.
- Add SpanPanelStaleDataError to the Error Handling table and explain
  the semantic distinction from SpanPanelConnectionError. Update the
  usage example to catch it during normal operation.
…n liveness

A snapshot-debounce timer scheduled just before a bridge disconnect
could fire after the fact and dispatch a snapshot built from the
still-ready accumulator — the panel-die case where no \$state=disconnected
message is published. Push consumers (e.g. the span HA integration)
then saw a spurious live snapshot while the bridge was actually down.

Two complementary fixes:
- _on_connection_change(False) now cancels any pending _snapshot_timer.
- _dispatch_snapshot is now gated by the same liveness predicate as
  get_snapshot() (bridge connected AND homie ready); stale dispatches
  bail silently with a debug log.

Amends 2.6.0 in place (not yet released).
Pre-existing drop-by surfaced while reviewing the liveness-signal work.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an explicit broker-connection edge callback API to the MQTT client and tightens snapshot semantics so consumers can reliably detect panel offline/online transitions (including preventing stale post-disconnect snapshot dispatch).

Changes:

  • Add SpanMqttClient.register_connection_callback() (also added to SpanPanelClientProtocol) and implement edge-only connection event fan-out with duplicate suppression.
  • Make get_snapshot() raise SpanPanelStaleDataError when the client is not fully live; guard _dispatch_snapshot() with the same liveness predicate and cancel pending debounce timers on disconnect.
  • Bump version to 2.6.0 and update docs/changelog plus comprehensive tests for the new behavior.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
uv.lock Updates locked project version to 2.6.0.
pyproject.toml Bumps package version to 2.6.0.
src/span_panel_api/mqtt/client.py Implements connection callback API, liveness-guarded get_snapshot(), and stale snapshot dispatch prevention.
src/span_panel_api/protocol.py Expands SpanPanelClientProtocol to include register_connection_callback.
src/span_panel_api/exceptions.py Adds SpanPanelStaleDataError to the exception hierarchy.
src/span_panel_api/init.py Exports SpanPanelStaleDataError from the top-level package.
README.md Documents connection state monitoring and updated error handling semantics.
CHANGELOG.md Adds 2.6.0 entry documenting the new API, behavioral change, and fix.
tests/test_mqtt_homie.py Adjusts tests for the new get_snapshot() liveness requirement by stubbing a connected bridge.
tests/test_mqtt_client_connection.py New tests for connection callback dispatch semantics, get_snapshot liveness guards, close behavior, and stale dispatch prevention.
tests/test_exceptions.py New tests for SpanPanelStaleDataError inheritance/distinction.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/span_panel_api/mqtt/client.py Outdated
… docstring

Copilot PR review flagged the 'call ping()' wording as misleading since
ping() is an async method. Changed to 'await ping()' to make the async
invocation explicit.
@cayossarian cayossarian merged commit 9e383bf into main Apr 16, 2026
5 checks passed
@cayossarian cayossarian deleted the fix/panel_off_line branch April 16, 2026 22:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants